package org.goldenport.g3.jetty

import scala.collection.JavaConversions._
import java.util.concurrent.{ConcurrentHashMap, CopyOnWriteArraySet}
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.ServletConfig
import javax.servlet.ServletException
import java.io.IOException
import com.asamioffice.goldenport.util.OptionUtil.lift
import org.eclipse.jetty.websocket.WebSocket
import org.eclipse.jetty.websocket.WebSocket._
import org.eclipse.jetty.websocket.WebSocketServlet
import org.goldenport.g3._
import org.goldenport.g3.servlet.G3Servlet

/**
 * @since   Nov. 20, 2010
 * @version Apr. 10, 2011
 * @author  ASAMI, Tomoharu
 */
class JettyWebSocketServlet(anApp: Option[G3Application]) extends WebSocketServlet {
  def this() = this(None)

  @Override
  protected def doWebSocketConnect(req: HttpServletRequest, protocol: String) = {
    new JettyWebSocket(getServletConfig, req, anApp)
  }
}

class JettyWebSocket(config: ServletConfig, req: HttpServletRequest, anApp: Option[G3Application]) extends WebSocket {
  private val _config: ServletConfig = config
  private val _path = req.getPathInfo()
  println("WebSocket pathInfo = " + _path)
  private var _g3app = {
    var app = anApp match {
      case Some(a) => a
      case None => {
        val appName = config.getInitParameter("g3.application") match {
          case name: String => name
          case null => "App"
          //case null => "org.goldenport.g3.app.Html5Service"  FIXME
        }
        Class.forName(appName).newInstance().asInstanceOf[G3Application]
      }
    }
    app.openServlet(config)
    app
  }
  private var _outbound: Option[Outbound] = None

  def this(c: ServletConfig, r: HttpServletRequest) = this(c, r, None)

  @Override
  def onConnect(outbound: Outbound) {
    println("WebSocket outbound: " + outbound)
    JettyWebSocketConnections.add(_path, outbound)
    _outbound = Some(outbound)
  }

  @Override
  def onDisconnect() {
    println("WebSocket outbound: " + _outbound)
    JettyWebSocketConnections.remove(_path, _outbound.get)
  }

  @Override
  def onMessage(frame: Byte, data: String) {
    try {
      _g3app.post(websocket_port_name, Some(data))
    } catch {
      case e: IOException => e.printStackTrace // XXX
    }
  }

  def websocket_port_name = "/ws" + _path // XXX

  @Override
  def onMessage(frame: Byte, data: Array[Byte], offset: Int, length: Int) {
    try {
      throw new UnsupportedOperationException("not implemented yet.")
    } catch {
      case e: IOException => e.printStackTrace // XXX
    }
  }

  // currently unused
  def sendMessage(data: String) {
    JettyWebSocketConnections.sendMessage(_path, data)
  }
}

object JettyWebSocketConnections {
  private val _outbounds = new ConcurrentHashMap[String, CopyOnWriteArraySet[Outbound]]
  private val _errors = new ConcurrentHashMap[String, CopyOnWriteArraySet[Outbound]]

  // FIXME two or moreconcurrent adding would causes missing ouboutnds
  // XXX use monitor
  def add(path: String, outbound: Outbound) {
    def is_ok: Boolean = {
      val set = _outbounds.get(path)
      set != null && set.contains(outbound)
    }

    do {
      val outbounds = _outbounds.get(path)
      if (outbounds != null) {
        outbounds.add(outbound)
      } else {
        val set = new CopyOnWriteArraySet[Outbound]
        set.add(outbound)
        _outbounds.putIfAbsent(path, set)
      }
    } while (!is_ok)
    clear_errors(path)
  }

  // FIXME concurrent clear would cause race condition.
  private def clear_errors(path: String) {
    val outbounds = _outbounds.get(path)
    if (outbounds != null) {
      for (obs <- lift(_errors.get(path)); ob <- obs) {
        outbounds.remove(ob)
      }
    }
  }

  // XXX use monitor
  def remove(path: String, outbound: Outbound) {
    val outbounds = _outbounds.get(path)
    outbounds.remove(outbound)
  }

  // FIXME in case of multiple access causes illegal states.
  // XX Use monitor
  def sendMessage(path: String, data: String) {
    println("JettyWebSocketConnections.sendMessage: " + path + ", " + data)
    for (obs <- lift(_outbounds.get(path)); ob <- obs) {
      try {
        ob.sendMessage(WebSocket.SENTINEL_FRAME, data)
      } catch {
        case e: IOException => {
          add_error(path, ob)
        }
      }
    }
  }

  private def add_error(path: String, outbound: Outbound) {
    def is_ok: Boolean = {
      val set = _errors.get(path)
      set != null && set.contains(outbound)
    }

    do {
      val errors = _errors.get(path)
      if (errors != null) {
        errors.add(outbound)
      } else {
        val set = new CopyOnWriteArraySet[Outbound]
        set.add(outbound)
        _errors.putIfAbsent(path, set)
      }
    } while (!is_ok)
  }
}
